<template>
  <div :class="classes">
    <b-input-number
        v-if="!range && showInput"
        :min="min"
        :size="inputSize"
        :max="max"
        :step="step"
        :value="exportValue[0]"
        :disabled="itemDisabled"
        :active-change="activeChange"
        @on-change="handleInputChange"></b-input-number>
    <div
        :class="[prefixCls + '-wrap']"
        ref="slider" @click.self="sliderClick"
    >
      <input type="hidden" :name="name" :value="exportValue">
      <div
          :class="[prefixCls + '-bar']"
          :style="barStyle"
          @click.self="sliderClick"></div>
      <template v-if="showStops">
        <div
            :class="[prefixCls + '-stop']"
            v-for="(item,index) in stops" :key="index"
            :style="{ 'left': item + '%' }"
            @click.self="sliderClick"
        ></div>
      </template>
      <template v-if="markList.length > 0">
        <div
            v-for="(item, key) in markList"
            :key="key"
            :class="[prefixCls + '-stop']"
            :style="{ 'left': item.position + '%' }"
            @click.self="sliderClick"
        ></div>
        <div class="bin-slider-marks">
          <SliderMarker
              v-for="(item, key) in markList"
              :key="key"
              :mark="item.mark"
              :style="{ 'left': item.position + '%' }"
              @click.native="sliderClick"
          />
        </div>
      </template>
      <div
          :class="[prefixCls + '-button-wrap']"
          :style="{left: minPosition + '%'}"
          @touchstart="onPointerDown($event, 'min')"
          @mousedown="onPointerDown($event, 'min')">
        <b-tooltip
            :controlled="pointerDown === 'min'"
            placement="top"
            :content="tipFormat(exportValue[0])"
            :disabled="tipDisabled"
            :always="showTip === 'always'"
            ref="minTooltip"
        >
          <div
              :class="minButtonClasses"
              tabindex="0"
              @focus="handleFocus('min')"
              @blur="handleBlur('min')"
              @keydown.left="onKeyLeft($event, 'min')"
              @keydown.down="onKeyLeft($event, 'min')"
              @keydown.right="onKeyRight($event, 'min')"
              @keydown.up="onKeyRight($event, 'min')"
          ></div>
        </b-tooltip>
      </div>
      <div v-if="range"
           :class="[prefixCls + '-button-wrap']"
           :style="{left: maxPosition + '%'}"
           @touchstart="onPointerDown($event, 'max')"
           @mousedown="onPointerDown($event, 'max')">
        <b-tooltip
            :controlled="pointerDown === 'max'"
            placement="top"
            :content="tipFormat(exportValue[1])"
            :disabled="tipDisabled"
            :always="showTip === 'always'"
            ref="maxTooltip"
        >
          <div
              :class="maxButtonClasses"
              tabindex="0"
              @focus="handleFocus('max')"
              @blur="handleBlur('max')"
              @keydown.left="onKeyLeft($event, 'max')"
              @keydown.down="onKeyLeft($event, 'max')"
              @keydown.right="onKeyRight($event, 'max')"
              @keydown.up="onKeyRight($event, 'max')"
          ></div>
        </b-tooltip>
      </div>
    </div>
  </div>
</template>

<script>
  import SliderMarker from './marker'
  import { getStyle, on, off } from '../../utils/dom'
  import { oneOf } from '../../utils/util'
  import Emitter from '../../mixins/emitter'
  import { addResizeListener, removeResizeListener } from '../../utils/resize-event'

  const prefixCls = 'bin-slider'

  export default {
    name: 'BSlider',
    mixins: [Emitter],
    components: { SliderMarker },
    props: {
      min: {
        type: Number,
        default: 0
      },
      max: {
        type: Number,
        default: 100
      },
      step: {
        type: Number,
        default: 1
      },
      range: {
        type: Boolean,
        default: false
      },
      value: {
        type: [Number, Array],
        default: 0
      },
      disabled: {
        type: Boolean,
        default: false
      },
      showInput: {
        type: Boolean,
        default: false
      },
      inputSize: {
        type: String,
        default: 'default',
        validator(value) {
          return oneOf(value, ['small', 'large', 'default'])
        }
      },
      showStops: {
        type: Boolean,
        default: false
      },
      tipFormat: {
        type: Function,
        default(val) {
          return val
        }
      },
      showTip: {
        type: String,
        default: 'hover',
        validator(value) {
          return oneOf(value, ['hover', 'always', 'never'])
        }
      },
      name: {
        type: String
      },
      activeChange: {
        type: Boolean,
        default: true
      },
      marks: {
        type: Object
      }
    },
    data() {
      const val = this.checkLimits(Array.isArray(this.value) ? this.value : [this.value])
      return {
        prefixCls: prefixCls,
        currentValue: val,
        dragging: false,
        pointerDown: '',
        startX: 0,
        currentX: 0,
        startPos: 0,
        oldValue: [...val],
        valueIndex: {
          min: 0,
          max: 1
        },
        sliderWidth: 0
      }
    },
    watch: {
      value(val) {
        val = this.checkLimits(Array.isArray(val) ? val : [val])
        if (!this.dragging && (val[0] !== this.currentValue[0] || val[1] !== this.currentValue[1])) {
          this.currentValue = val
        }
      },
      exportValue(values) {
        this.$nextTick(() => {
          this.$refs.minTooltip.updatePopper()
          if (this.range) {
            this.$refs.maxTooltip.updatePopper()
          }
        })
        const value = this.range ? values : values[0]
        this.$emit('input', value)
        this.$emit('on-input', value)
      }
    },
    computed: {
      classes() {
        return [
          `${prefixCls}`,
          {
            [`${prefixCls}-input`]: this.showInput && !this.range,
            [`${prefixCls}-range`]: this.range,
            [`${prefixCls}-disabled`]: this.itemDisabled
          }
        ]
      },
      minButtonClasses() {
        return [
          `${prefixCls}-button`,
          {
            [`${prefixCls}-button-dragging`]: this.pointerDown === 'min'
          }
        ]
      },
      maxButtonClasses() {
        return [
          `${prefixCls}-button`,
          {
            [`${prefixCls}-button-dragging`]: this.pointerDown === 'max'
          }
        ]
      },
      exportValue() {
        const decimalCases = (String(this.step).split('.')[1] || '').length
        return this.currentValue.map(nr => Number(nr.toFixed(decimalCases)))
      },
      minPosition() {
        const val = this.currentValue
        return (val[0] - this.min) / this.valueRange * 100
      },
      maxPosition: function () {
        const val = this.currentValue

        return (val[1] - this.min) / this.valueRange * 100
      },
      barStyle() {
        const style = {
          width: (this.currentValue[0] - this.min) / this.valueRange * 100 + '%'
        }

        if (this.range) {
          style.left = (this.currentValue[0] - this.min) / this.valueRange * 100 + '%'
          style.width = (this.currentValue[1] - this.currentValue[0]) / this.valueRange * 100 + '%'
        }

        return style
      },
      stops() {
        let stopCount = this.valueRange / this.step
        let result = []
        let stepWidth = 100 * this.step / this.valueRange
        for (let i = 1; i < stopCount; i++) {
          result.push(i * stepWidth)
        }
        return result
      },
      markList() {
        if (!this.marks) return []

        const marksKeys = Object.keys(this.marks)
        return marksKeys.map(parseFloat)
          .sort((a, b) => a - b)
          .filter(point => point <= this.max && point >= this.min)
          .map(point => ({
            point,
            position: (point - this.min) * 100 / (this.max - this.min),
            mark: this.marks[point]
          }))
      },
      tipDisabled() {
        return this.tipFormat(this.currentValue[0]) === null || this.showTip === 'never'
      },
      valueRange() {
        return this.max - this.min
      },
      firstPosition() {
        return this.currentValue[0]
      },
      secondPosition() {
        return this.currentValue[1]
      },
      itemDisabled() {
        return this.disabled
      }
    },
    methods: {
      getPointerX(e) {
        return e.type.indexOf('touch') !== -1 ? e.touches[0].clientX : e.clientX
      },
      checkLimits([min, max]) {
        min = Math.max(this.min, min)
        min = Math.min(this.max, min)

        max = Math.max(this.min, min, max)
        max = Math.min(this.max, max)
        return [min, max]
      },
      getCurrentValue(event, type) {
        if (this.itemDisabled) {
          return
        }

        const index = this.valueIndex[type]
        if (typeof index === 'undefined') {
          return
        }

        return this.currentValue[index]
      },
      onKeyLeft(event, type) {
        const value = this.getCurrentValue(event, type)
        if (Number.isFinite(value)) {
          this.changeButtonPosition(value - this.step, type)
        }
      },
      onKeyRight(event, type) {
        const value = this.getCurrentValue(event, type)
        if (Number.isFinite(value)) {
          this.changeButtonPosition(value + this.step, type)
        }
      },
      onPointerDown(event, type) {
        if (this.itemDisabled) return
        event.preventDefault()
        this.pointerDown = type

        this.onPointerDragStart(event)
        on(window, 'mousemove', this.onPointerDrag)
        on(window, 'touchmove', this.onPointerDrag)
        on(window, 'mouseup', this.onPointerDragEnd)
        on(window, 'touchend', this.onPointerDragEnd)
      },
      onPointerDragStart(event) {
        this.dragging = false
        this.startX = this.getPointerX(event)
        this.startPos = (this[`${this.pointerDown}Position`] * this.valueRange / 100) + this.min
      },
      onPointerDrag(event) {
        this.dragging = true
        this.$refs[`${this.pointerDown}Tooltip`].visible = true
        this.currentX = this.getPointerX(event)
        const diff = (this.currentX - this.startX) / this.sliderWidth * this.valueRange

        this.changeButtonPosition(this.startPos + diff)
      },
      onPointerDragEnd() {
        if (this.dragging) {
          this.dragging = false
          this.$refs[`${this.pointerDown}Tooltip`].visible = false
          this.emitChange()
        }

        this.pointerDown = ''
        off(window, 'mousemove', this.onPointerDrag)
        off(window, 'touchmove', this.onPointerDrag)
        off(window, 'mouseup', this.onPointerDragEnd)
        off(window, 'touchend', this.onPointerDragEnd)
      },
      changeButtonPosition(newPos, forceType) {
        const type = forceType || this.pointerDown
        const index = type === 'min' ? 0 : 1
        if (type === 'min') {
          newPos = this.checkLimits([newPos, this.max])[0]
        } else {
          newPos = this.checkLimits([this.min, newPos])[1]
        }

        const modulus = this.handleDecimal(newPos, this.step)
        const value = this.currentValue
        value[index] = newPos - modulus

        // 判断左右是否相等，否则会出现左边大于右边的情况
        if (this.range) {
          if (type === 'min' && value[0] > value[1]) value[1] = value[0]
          if (type === 'max' && value[0] > value[1]) value[0] = value[1]
        }

        this.currentValue = [...value]

        if (!this.dragging) {
          if (this.currentValue[index] !== this.oldValue[index]) {
            this.emitChange()
            this.oldValue[index] = this.currentValue[index]
          }
        }
      },
      handleDecimal(pos, step) {
        if (step < 1) {
          let sl = step.toString()
          let multiple = 1
          let m
          try {
            m = sl.split('.')[1].length
          } catch (e) {
            m = 0
          }
          multiple = Math.pow(10, m)
          return (pos * multiple) % (step * multiple) / multiple
        } else {
          return pos % step
        }
      },
      emitChange() {
        const value = this.range ? this.exportValue : this.exportValue[0]
        this.$emit('on-change', value)
        this.dispatch('BFormItem', 'on-form-change', value)
      },

      sliderClick(event) {
        if (this.itemDisabled) return
        const currentX = this.getPointerX(event)
        const sliderOffsetLeft = this.$refs.slider.getBoundingClientRect().left
        let newPos = ((currentX - sliderOffsetLeft) / this.sliderWidth * this.valueRange) + this.min
        let regularNewPos = newPos / this.valueRange * 100

        if (!this.range || regularNewPos <= this.minPosition) {
          this.changeButtonPosition(newPos, 'min')
        } else if (regularNewPos >= this.maxPosition) {
          this.changeButtonPosition(newPos, 'max')
        } else {
          this.changeButtonPosition(newPos, ((newPos - this.firstPosition) <= (this.secondPosition - newPos)) ? 'min' : 'max')
        }
      },

      handleInputChange(val) {
        this.currentValue = [val === 0 ? 0 : val || this.min, this.currentValue[1]]
        this.emitChange()
      },

      handleFocus(type) {
        this.$refs[`${type}Tooltip`].handleShowPopper()
      },

      handleBlur(type) {
        this.$refs[`${type}Tooltip`].handleClosePopper()
      },
      handleSetSliderWidth() {
        this.sliderWidth = parseInt(getStyle(this.$refs.slider, 'width'), 10)
      }
    },
    mounted() {
      // #2852
      this.$on('on-visible-change', (val) => {
        if (val && this.showTip === 'always') {
          this.$refs.minTooltip.doDestroy()
          if (this.range) {
            this.$refs.maxTooltip.doDestroy()
          }
          this.$nextTick(() => {
            this.$refs.minTooltip.updatePopper()
            if (this.range) {
              this.$refs.maxTooltip.updatePopper()
            }
          })
        }
      })
      addResizeListener(this.$refs.slider, this.handleSetSliderWidth)
    },
    beforeDestroy() {
      removeResizeListener(this.$refs.slider, this.handleSetSliderWidth)
    }
  }
</script>
